/* 
  请描述Vue2中响应式原理
    - Vue响应式指的是:组件的data数据发生改变,立刻就会触发视图的更新
    - 响应式使用的就是:发布-订阅模式(也称作观察者模式)主要是:数据劫持 + 依赖收集
    - 数据劫持的原理:
      Vue2通过Object.defineProperty来进行数据劫持,把data中每一个对象的属性使用defineProperty重写getter和setter,
      具体做法书写一个Observer类,其实就是发布订阅模式中的发布器,主要功能就是对数据进行劫持,并在getter和setter中完成功能,
      在getter中收集依赖,在setter中通知依赖进行视图更新
    - 依赖收集原理:
      - 有一个Dep类,其实就是发布订阅模型的订阅器,Dep上有收集依赖和通知依赖更新的方法,每一个数据在劫持的时候,创建一个dep实例,
      在getter中调用dep实例的收集依赖方法,在setter中调用dep实例的通知依赖更新方法
      - 有一个Watcher类,Wacther身上有获取数据的方法,和重新获取数据并更新视图的update方法.每次模板中编译data数据的时候,
      都会创建一个对应的watcher实例,watcher实例会在第一次获取时候的时候被收集到当前数据的dep中,在数据更新时,会通知所有watcher调用其身上的update方法,
      达到响应式的目的

*/

//定义一个全部变量 用来保存当前正在请求数据的watcher
let W = null;

//Watcher类其实就是发布订阅模式中的订阅者,只有模板解析的时候用到了属性,才会创建一个订阅者(每一个模板的数据对应一个订阅者watcher)
class Watcher {
  constructor(data, key) {
    this.data = data;
    this.key = key;
    //因为在获取值的时候,数据劫持中会收集我这个watcher实例,所以我要把watcher实例放在全局变量W上,方便人家获取到
    W = this;
    this.get();
    //当依赖被收集以后,请把W还出去,否则下次不用watcher获取值的时候也会把我这个watcher收集进去
    W = null;
  }

  //因为new Watcher其实就是为了得到一个值  并 把自己添加在dep的依赖中,所以我们封装一个获取值的函数
  get() {
    //当我们拿值的时候,会直接进入数据劫持的getter中,getter帮我们返回一个数据,并且把当前的watcher添加到人家数据的dep依赖收集中
    return this.data[this.key];
  }

  //给wacther设置一个方法,当watcher调用这个方法的时候,就能重新去获取新的数据,并重新渲染模板(Compiler)
  update() {
    console.log(
      `${
        this.key
      }的数据发生更新了,我们重新获取一下吧${this.get()},并重新渲染一下视图`
    );
  }
}

//Dep就是发布订阅模型中的订阅器,拥有两个方法,收集所有依赖,通知所有依赖
class Dep {
  constructor() {
    this.subs = [];
  }

  addDeppend(watcher) {
    this.subs.push(watcher);
  }

  notify() {
    this.subs.forEach((watcher) => {
      watcher.update();
    });
  }
}

function defineReactive(data, key, value) {
  //直接递归调用observe 传入value,查看是否需要深层次的响应式
  observe(value);

  // 在对每一个属性进行劫持的时候,都要为每一个属性创建一个Dep实例,用来收集依赖和通知依赖
  const dep = new Dep();

  //对当前的data 中的 key  进行数据响应式操作(Object.defineProperty的getter和setter)
  Object.defineProperty(data, key, {
    get: function ReactiveGetter() {
      //只要是Wacther(订阅者)来拿数据的时候,我们需要把当前的订阅者收集到依赖里(Dep:是发布订阅的中间人) 可以提供一个数组,保存当前数据的依赖项,还提供了一个通知所有依赖项的方法
      console.log("在数据劫持的getter中获取数据", key);

      //调用dep中的收集依赖方法,要判断是watcher访问还是其他的方式访问,只有watcher的访问才能收集依赖
      if (W) {
        dep.addDeppend(W);
      }

      return value;
    },
    set: function ReactiveSetter(newVal) {
      console.log("在数据劫持的Setter中收到了设置数据", key);

      value = newVal;

      //当触发setter更新的时候,要通知当前数据dep上的保存的所有依赖
      dep.notify();
    },
  });
}

//Observer类就是发布-订阅模型中的发布者
class Observer {
  constructor(_data) {
    this._data = _data;

    //判断是数组还是对象,如果是对象,则直接开始数据响应式操作
    // 如果是数组,则遍历数组查看数组内部是否有对象类型,需要进一步响应式处理
    //我们只是对数组内部是否有对象进行检测,而不会关数组中的某个基本类型是否改变,或者数组的长度是否改变
    //也就是说,我们还是给数组中的对象进行响应式操作的
    if (Array.isArray(this._data)) {
      this.observerArray(this._data);
    } else {
      this.walk(this._data);
    }
  }
  //observerArray方法,当数据是数组的时候,会进行下一步处理
  observerArray(_data) {
    _data.forEach((item) => observe(item));
  }

  walk(_data) {
    //当进入walk的时候,数据此时一定是一个对象,我们要对对象进行遍历,对内部的属性一个个的进行响应式操作
    for (let key of Object.keys(_data)) {
      //在这个位置对一个个的属性响应式操作,我们把操作封装到defineReactive函数中处理
      defineReactive(_data, key, _data[key]);
    }
  }
}

function observe(_data) {
  //基本类型值直接不进行任何处理了
  if (typeof _data !== "object" || _data === null) return;

  //如果进入observe的是对象类型,则我们需要开始对这个对象数据响应式处理
  new Observer(_data);
}

function Vue(options) {
  //先把数据放在_data上
  this._data = options.data;

  //数据代理
  for (let key of Object.keys(this._data)) {
    Object.defineProperty(this, key, {
      get: function proxyGetter() {
        return this._data[key];
      },
      set: function proxySetter(newVal) {
        this._data[key] = newVal;
      },
    });
  }

  //observe函数中:其实是为了判断需要被响应式处理的类型,如果是基本类型值,不在进行响应式处理
  //在observe内部 使用实例化Observer对数据进行响应式处理
  observe(this._data);
}

/* 




*/
//测试使用
const vm = new Vue({
  data: {
    count: 1,
    student: {
      name: "laowang",
      age: 18,
    },
    food: [
      { id: "001", type: "热干面" },
      { id: "002", type: "烧饼" },
    ],
  },
});

function huoqu() {
  // new Watcher(vm, "count");
  // new Watcher(vm.student, "name");
  new Watcher(vm.food[0], "type");
}

huoqu();

function shezhi() {
  // vm.count = 2;
  // vm.student.name = "xiaoli";
  vm.food[0].type = "焖面";
}
shezhi();
